/*
 * Copyright (c) 2011 Imre Fazekas.
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the name of the Brillien nor the names of its
 * terms and concepts may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.brillien.ignition.transport;

import com.vii.brillien.kernel.BrillienException;
import com.vii.brillien.kernel.axiom.sso.Session;
import com.vii.brillien.kernel.axiom.transport.Communication;
import com.vii.streamline.services.TypeServices;
import com.vii.streamline.services.error.StreamLineException;
import com.vii.streamline.services.json.JsonServices;
import com.vii.streamline.services.reflection.ReflectionServices;
import com.vii.streamline.structures.collections.InnerMap;
import org.svenson.JSONProperty;

import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Simple implementation for the Message axiom
 */
public class BrillienCommunication implements Communication {

    /**
     * Unique id of the message
     */
    private String                  id;

    /**
     * Unique name of the sender
     */
    private String                  sender;

    /**
     * Unique name of the recipient
     */
    private String                  recipient;

    /**
     * Stores the type of the original message if this is a response.
     */
    private int                     messageType;

    /**
     * ID of the given flow instance
     */
    private String                  flowID;

    /**
     * ID of the given session where this message participate
     */
    private Session                 session;

    /**
     * API_KEY if required for the call
     */
    private String                  apiKey;

    /**
     * Name of the message
     */
    private String                  subject;

    /**
     * Name of the redirection recipient entity if exists.
     */
    private String                  redirectEntityName;

    /**
     * Name of the return message if exists.
     */
    private String                  redirectMessage;

    /**
     * Timestamp marking the date of sending.
     */
    private Long                    requestTime;

    /**
     * Timestamp marking the date of responding.
     */
    private Long                    responseTime;

    /**
     * Parameters of the message.
     */
    private Map<String, Object>     parameters;

    /**
     * Response object if exits.
     */
    private Object                  response;

    /**
     * Stores the type of the original packet if this is a response.
     */
    private int                     originalType;

    /**
     * Stores the id of the original packet if this is a response.
     */
    private String                  originalId;

    /**
     * code of the error if this is an error message and the code has been given
     */
    private Integer                 errorCode;

    /**
     * additional value of the error if this is an error message and the value has been given
     */
    private Object                  errorValue;

    /**
     * Storage of extensions
     */
    protected ConcurrentHashMap<String, Object> extension;


    {
        originalId = id = TransportStatics.newMessageId();

        messageType = originalType = Communication.TYPE_UNDEFINED;
        parameters = new InnerMap<String, Object>();

        setRequestTime( System.currentTimeMillis() );
        setResponseTime( new Long(-1) );

        flowID = DEFAULT_FLOW_ID;

        extension = new ConcurrentHashMap<String, Object>();
    }

    public BrillienCommunication() {
    }

    public BrillienCommunication(Integer errorCode, Object response, Object errorValue) {
        this.errorValue = errorValue;
        this.errorCode = errorCode;
        this.response = response;
        this.setMessageType( Communication.TYPE_ERROR );
    }

    /**
     * Getter method for field id
     */
    @Override
    public String getId() {
        return id;
    }

    /**
     * Setter method for field id
     */
    @Override
    public void setId(String id) {
        this.id = id;
    }

    /**
     * Getter method for field originalType
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public int getMessageType() {
        return messageType;
    }

    /**
     * Setter method for field originalType
     */
    @Override
    public void setMessageType(int messageType) {
        this.messageType = messageType;
    }

    /**
     * Getter method for field sender
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public String getSender() {
        return sender;
    }

    /**
     * Setter method for field sender
     */
    @Override
    public void setSender(String sender) {
        this.sender = sender;
    }

    /**
     * Getter method for field recipientName
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public String getRecipient() {
        return recipient;
    }

    /**
     * Setter method for field recipient
     */
    @Override
    public void setRecipient(String recipient) {
        this.recipient = recipient;
    }

    /**
     * Getter method for field flowID
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public String getFlowID() {
        return flowID;
    }

    /**
     * Setter method for field flowID
     */
    @Override
    public void setFlowID(String flowID) {
        this.flowID = (flowID == null || flowID.equals(""))
                ? DEFAULT_FLOW_ID
                : flowID;
    }

    /**
     * Getter method for field session
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public Session getSession() {
        return session;
    }

    /**
     * Setter method for field session
     */
    @Override
    public void setSession(Session session) {
        this.session = session;
    }


    /**
     * Getter method for field subject
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public String getApiKey() {
        return apiKey;
    }

    /**
     * Setter method for field session
     */
    @Override
    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }

    /**
     * Getter method for field subject
     */
    @Override
    public String getSubject() {
        return subject;
    }

    /**
     * Setter method for field subject
     */
    @Override
    public void setSubject(String subject) {
        this.subject = subject;
    }

    /**
     * Getter method for field redirectEntityName
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public String getRedirectEntityName() {
        return redirectEntityName;
    }

    /**
     * Setter method for field redirectEntityName
     */
    @Override
    public void setRedirectEntityName(String redirectEntityName) {
        this.redirectEntityName = redirectEntityName;
    }

    /**
     * Getter method for field redirectMessage
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public String getRedirectMessage() {
        return redirectMessage;
    }

    /**
     * Setter method for field redirectMessage
     */
    @Override
    public void setRedirectMessage(String redirectMessage) {
        this.redirectMessage = redirectMessage;
    }

    /**
     * Getter method for field requestTime
     */
    @Override
    @JSONProperty(ignore = true)
    public Long getRequestTime() {
        return requestTime;
    }

    /**
     * Setter method for field requestTime
     */
    @Override
    public void setRequestTime(Long requestTime) {
        this.requestTime = requestTime;
    }

    /**
     * Getter method for field responseTime
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public Long getResponseTime() {
        return responseTime;
    }

    /**
     * Getter method for field responseTime
     */
    @Override
    public void setResponseTime(Long responseTime) {
        this.responseTime = responseTime;
    }

    /**
     * Getter method for field parameters
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public Map<String, Object> getParameters() {
        return parameters;
    }

    /**
     * Setter method for field parameters
     */
    @Override
    public void setParameters(Map<String, Object> parameters) {
        this.parameters = parameters;
    }

    @Override
    public Map<String, Object> acquireParameters(Map<String, Class> types) throws BrillienException{
        InnerMap<String, Object> realParameters = new InnerMap<String, Object>();
        for( String name : parameters.keySet() )
            try {
                realParameters.put( name, types.get(name) != null
                        ? TypeServices.isSimpleType( types.get(name) ) ? parameters.get(name) : JsonServices.parseJSON( JsonServices.toJSON(parameters.get(name)), types.get(name) )
                        : parameters.get(name)
                );
            } catch (StreamLineException e) {
                throw new BrillienException( e.getErrorCode(), e.getMessage(), e.getPayLoad() );
            }
        return realParameters;
    }

    public Object[] acquireParameters(Method m) throws BrillienException{
        try {
            List<String> formalParameterNames = ReflectionServices.getFormalParameterNames(m);
            List<Class> formalParameterTypes = ReflectionServices.getFormalParameterTypes(m);
            Map<String,Class> res = new LinkedHashMap<String,Class>();
            for( int i=0; i<formalParameterNames.size(); ++i )
                res.put(
                        formalParameterNames.get(i),
                        formalParameterTypes.get(i)
                );
            Map<String, Object> objects = this.acquireParameters( res );

            Object[] parameters = new Object[ formalParameterNames.size() ];
            for( int i=0; i<parameters.length; ++i )
                parameters[i] = objects.containsKey( formalParameterNames.get(i) ) ? objects.get( formalParameterNames.get(i) ) : null;
            return parameters;
        } catch (StreamLineException e) {
            throw new BrillienException( e.getErrorCode(), e.getMessage(), e.getPayLoad() );
        }
    }

    /**
     * Getter method for field response
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public <T> T getResponse() {
        return (T)response;
    }

    /**
     * Setter method for field response
     */
    @Override
    public void setResponse(Object response) {
        this.response = response;
    }

    @Override
    public <T> T acquireResponse(Class<T> type) throws BrillienException {
        try {
            return TypeServices.isSimpleType(type) ? (T)getResponse() : JsonServices.<T>parseJSON( JsonServices.toJSON(response), type );
        } catch (StreamLineException e) {
            throw new BrillienException( e.getErrorCode(), e.getMessage(), e.getPayLoad() );
        }
    }

    @Override
    public <T> T acquireResponse(Class<T> type, Map<String, Class> typeHints) throws BrillienException{
        try {
            return JsonServices.<T>parseJSON( JsonServices.toJSON(response), type, typeHints );
        } catch (StreamLineException e) {
            throw new BrillienException( e.getErrorCode(), e.getMessage(), e.getPayLoad() );
        }
    }

    @Override
    public <T> T acquireResponse(Map<String, Class> typeHints) throws BrillienException{
        try {
            return JsonServices.<T>parseJSON( JsonServices.toJSON(response), typeHints );
        } catch (StreamLineException e) {
            throw new BrillienException( e.getErrorCode(), e.getMessage(), e.getPayLoad() );
        }
    }


    @Override
    public <T> T acquireResponse(String typeHint, Class hintClass) throws BrillienException{
        try {
            return JsonServices.<T>parseJSON( JsonServices.toJSON(response), typeHint, hintClass );
        } catch (StreamLineException e) {
            throw new BrillienException( e.getErrorCode(), e.getMessage(), e.getPayLoad() );
        }
    }

    @Override
    public <T> T acquireResponse(Class<T> type, String typeHint, Class hintClass) throws BrillienException{
        try {
            return JsonServices.<T>parseJSON( JsonServices.toJSON(response), type, typeHint, hintClass );
        } catch (StreamLineException e) {
            throw new BrillienException( e.getErrorCode(), e.getMessage(), e.getPayLoad() );
        }
    }

    /**
     * Getter method for field originalType
     */
    @Override
    @JSONProperty(ignoreIfNull = true)
    public int getOriginalType() {
        return originalType;
    }

    /**
     * Setter method for field originalType
     */
    @Override
    public void setOriginalType(int originalType) {
        this.originalType = originalType;
    }

    @JSONProperty(ignoreIfNull = true)
    public String getOriginalId() {
        return originalId;
    }

    @Override
    public void setOriginalId(String originalId) {
        this.originalId = originalId;
    }

    @Override
    @JSONProperty(ignoreIfNull = true)
    public Integer getErrorCode() {
        return errorCode;
    }

    @Override
    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }

    @Override
    @JSONProperty(ignoreIfNull = true)
    public Object getErrorValue() {
        return errorValue;
    }

    @Override
    public void setErrorValue(Object errorValue) {
        this.errorValue = errorValue;
    }

    @Override
    @JSONProperty(ignore = true)
    public boolean isAnswerTo(Communication msg){
        return originalId.equals( msg.getId() );
    }

    @Override
    public void addExtension(String key, Object value){
        extension.putIfAbsent( key, value );
    }

    @JSONProperty(ignore = true)
    @Override
    public <T> T getExtension(String key){
        return (T)extension.get( key );
    }

    @Override
    public boolean hasExcension(String key){
        return key != null && extension.containsKey( key );
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BrillienCommunication)) return false;

        BrillienCommunication that = (BrillienCommunication) o;

        if (id != null ? !id.equals(that.id) : that.id != null) return false;

        return true;
    }

    public <C extends BrillienCommunication> C newInstance() throws BrillienException {
        try {
            return (C)this.getClass().newInstance();
        } catch (Exception e) {
            throw new BrillienException(StreamLineException.ERROR_ILLEGAL_INNER_STATE, "Cannot initiate BrillienCommunication instance!", e);
        }
    }
    
    public <C extends BrillienCommunication> C prepareForResponse(Object response) throws BrillienException {
        BrillienCommunication comm = newInstance();

        comm.setId( TransportStatics.newMessageId() );
        comm.setOriginalId(this.getId());
        comm.setMessageType( this.getMessageType() == TYPE_DELEGATED_SET ? TYPE_SET : TYPE_RESULT );
        comm.setOriginalType(this.getMessageType());

        String sender = this.getRecipient();
        String recipient = this.getRedirectEntityName() == null
                ? getSender()
                : this.getRedirectEntityName();
        comm.setSender(sender);
        comm.setRecipient(recipient);

        comm.setFlowID( this.getFlowID() );

        comm.setSession( this.getSession() );

        comm.setSubject( this.getSubject() );

        comm.setRedirectEntityName( this.getRedirectEntityName() );

        comm.setRedirectMessage( this.getRedirectMessage() );

        comm.setRequestTime( this.getRequestTime() );

        comm.setResponseTime( System.currentTimeMillis() );

        comm.setParameters( this.getParameters() );

        if( response != null )
            comm.setResponse(response);
        
        return (C)comm;
    }

    public <C extends BrillienCommunication> C prepareForError( String message ) throws BrillienException {
        try{
            BrillienCommunication comm = prepareForResponse( message );

            comm.setMessageType( Communication.TYPE_ERROR );

            return (C)comm;
        } catch (Exception e){
            throw new BrillienException( e );
        }
    }

    public <C extends BrillienCommunication> C prepareForError( int errorCode, String message, Object value ) throws BrillienException{
        try{
            BrillienCommunication comm = prepareForResponse( message );

            comm.setMessageType( Communication.TYPE_ERROR );

            comm.setErrorCode( errorCode );

            if( value != null )
                comm.setErrorValue( value );

            return (C)comm;
        } catch (Exception e){
            throw new BrillienException( e );
        }
    }

    public static BrillienCommunication createCommunication( String sender, String recipient, String subject, int type, Map<String, Object> parameters) {
        return createCommunication( null,sender, recipient, subject, type, parameters );
    }
    public static BrillienCommunication createCommunication(Session session, String sender, String recipient, String subject, int type, Map<String, Object> parameters) {
        BrillienCommunication newComm = new BrillienCommunication();

        newComm.setSession(session);
        newComm.setSender(sender);
        newComm.setRecipient(recipient);
        newComm.setSubject(subject);
        newComm.setMessageType(type);

        if (parameters != null)
            newComm.setParameters(parameters);

        return newComm;
    }


        @Override
    public String toString() {
        return "BrillienCommunication{" +
                "id='" + id + '\'' +
                ", sender='" + sender + '\'' +
                ", subject='" + subject + '\'' +
                ", recipient='" + recipient + '\'' +
                ", messageType=" + messageType +
                ", flowID='" + flowID + '\'' +
                ", session=" + session +
                ", apiKey=" + apiKey +
                ", redirectEntityName='" + redirectEntityName + '\'' +
                ", redirectMessage='" + redirectMessage + '\'' +
                ", requestTime=" + requestTime +
                ", responseTime=" + responseTime +
                ", parameters=" + parameters +
                ", response='" + response + '\'' +
                ", originalType=" + originalType +
                ", originalId=" + originalId +
                ", errorCode=" + errorCode +
                ", errorValue=" + errorValue +
                ", extension=" + extension +
                '}';
    }
}
